18.7 系统调用
为支持并发调度,Go专门对syscall、cgo进行了包装,以便在长时间阻塞时能切换执行其他任务。在标准库syscall包里,将系统调用函数分为Syscall和RawSyscall两类。
src/syscall/zsyscall_linux_amd64.s
func Getcwd(buf []byte) (n int, err error) { r0, _, e1 := Syscall(SYS_GETCWD, uintptr(_p0), uintptr(len(buf)), 0) } func EpollCreate(size int) (fd int, err error) { r0, _, e1 := RawSyscall(SYS_EPOLL_CREATE, uintptr(size), 0, 0) }
让我们看看这两者有什么区别。
src/syscall/asm_linux_amd64.s
TEXT •Syscall(SB),NOSPLIT,0-56 MOVQ trap+0(FP), AX // syscall entry SYSCALL JLS ok1 RET ok1: RET
最大的不同在于Syscall增加了entrysyscall/exitsyscall,这就是允许调度的关键所在。
proc1.go
func entersyscall(dummy int32) { reentersyscall(getcallerpc(unsafe.Pointer(&dummy)), getcallersp(unsafe.Pointer(&dummy))) } func reentersyscall(pc, sp uintptr) { g := getg() // 保存执行现场 save(pc, sp) g.syscallsp = sp g.syscallpc = pc casgstatus(g, _Grunning, _Gsyscall) // 确保 sysmon 运行 if atomicload(&sched.sysmonwait) != 0 { systemstack(entersyscall_sysmon) save(pc, sp) } // 设置相关状态 g.m.syscalltick = g.m.p.ptr().syscalltick g.sysblocktraced = true g.m.mcache = nil g.m.p.ptr().m = 0 atomicstore(&g.m.p.ptr().status, _Psyscall) }
监控线程sysmon对syscall非常重要,因为它负责将因系统调用而长时间阻塞的P抢回,用于执行其他任务。否则,整体性能会严重下降,甚至整个进程都会被冻结。
proc1.go
func entersyscall_sysmon() { if atomicload(&sched.sysmonwait) != 0 { atomicstore(&sched.sysmonwait, 0) notewakeup(&sched.sysmonnote) } }
某些系统调用本身就可以确定长时间阻塞(比如锁),那么它会选择执行entersyscallblock主动交出所关联的P。
proc1.go
func entersyscallblock(dummy int32) { casgstatus(g, _Grunning, _Gsyscall) systemstack(entersyscallblock_handoff) } func entersyscallblock_handoff() { // 释放 P,让它去执行其他任务 handoffp(releasep()) } func handoffp(p *p) { // 如果 P 本地或全局有任务,直接唤醒某个 M 开始工作 if !runqempty(p) || sched.runqsize != 0 { startm(p, false) return } … // 没有任务就放回空闲队列 pidleput(p) }
从系统调用返回时,必须检查P是否依然可用,因为可能已被sysmon抢走。
proc1.go
func exitsyscall(dummy int32) { g := getg() oldp := g.m.p.ptr() if exitsyscallfast() { casgstatus(g, _Gsyscall, _Grunning) return } mcall(exitsyscall0) }
快速退出exitsyscallfast是指能重新绑定原有或空闲的P,以继续当前G任务的执行。
proc1.go
func exitsyscallfast() bool { g := getg() // STW 状态,就不要继续了 if sched.stopwait == freezeStopWait { g.m.mcache = nil g.m.p = 0 return false } // 尝试关联原本的 P if g.m.p != 0 && g.m.p.ptr().status == _Psyscall && cas(&g.m.p.ptr().status, _Psyscall, _Prunning) { g.m.mcache = g.m.p.ptr().mcache g.m.p.ptr().m.set(g.m) return true } // 获取其他空闲 P oldp := g.m.p.ptr() g.m.mcache = nil g.m.p = 0 if sched.pidle != 0 { var ok bool systemstack(func() { ok = exitsyscallfast_pidle() }) if ok { return true } } return false } func exitsyscallfast_pidle() bool { p := pidleget() // 唤醒 sysmon if p != nil && atomicload(&sched.sysmonwait) != 0 { atomicstore(&sched.sysmonwait, 0) notewakeup(&sched.sysmonnote) } // 重新关联 if p != nil { acquirep(p) return true } return false }
如果多次尝试绑定P却失败,那么只能将当前任务放入待运行队列。
proc1.go
func exitsyscall0(gp *g) { g := getg() // 修改状态,解除和 M 的关联 casgstatus(gp, _Gsyscall, _Grunnable) dropg() // 再次获取空闲 P p := pidleget() if p == nil { // 获取失败,放回全局任务队列 globrunqput(gp) } else if atomicload(&sched.sysmonwait) != 0 { atomicstore(&sched.sysmonwait, 0) notewakeup(&sched.sysmonnote) } // 再次检查 P,以便执行当前任务 if p != nil { acquirep(p) execute(gp, false) // Never returns. } // 关联 P 失败,休眠当前 M stopm() schedule() // Never returns. }
需要注意,cgo使用了相同的封装方式,因为它同样不受调度器管理。
cgocall.go
func cgocall(fn, arg unsafe.Pointer) int32 { /* * Announce we are entering a system call * so that the scheduler knows to create another M to run goroutines while we are in * the foreign code. * * The call to asmcgocall is guaranteed not to split the stack and does not allocate * memory, so it is safe to call while “in a system call”, outside the $GOMAXPROCS * accounting. */ entersyscall(0) errno := asmcgocall(fn, arg) exitsyscall(0) }